package main

import (
	"os"

	"context"
	"encoding/json"
	"fmt"
	"strings"

	alidns "github.com/alibabacloud-go/alidns-20150109/v2/client"
	openapi "github.com/alibabacloud-go/darabonba-openapi/client"
	"github.com/alibabacloud-go/tea/tea"

	extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	apis_metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"

	"github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
	"github.com/jetstack/cert-manager/pkg/acme/webhook/cmd"
	metav1 "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
	"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
)

var GroupName = os.Getenv("GROUP_NAME")

func main() {
	if GroupName == "" {
		panic("GROUP_NAME must be specified")
	}

	// This will register our aliyun DNS provider with the webhook serving
	// library, making it available as an API under the provided GroupName.
	// You can register multiple DNS provider implementations with a single
	// webhook, where the Name() method will be used to disambiguate between
	// the different implementations.
	cmd.RunWebhookServer(GroupName,
		&aliyunDNSProviderSolver{},
	)
}

type aliyunDNSProviderSolver struct {
	client     *kubernetes.Clientset
	dnsClients map[string]*alidns.Client
}

type aliyunDNSProviderConfig struct {
	Endpoint        string                   `json:"endpoint"`
	AccessKey       string                   `json:"accessKey"`
	AccessKeyRef    metav1.SecretKeySelector `json:"accessKeyRef"`
	AccessSecret    string                   `json:"accessSecret"`
	AccessSecretRef metav1.SecretKeySelector `json:"accessSecretRef"`
}

func (c *aliyunDNSProviderSolver) Name() string {
	return "aliyundns"
}

func (c *aliyunDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
	cfg, err := loadConfig(ch.Config)
	if err != nil {
		return err
	}

	// TODO: add code that sets a record in the DNS provider's console
	dnsClient, err := c.getDnsClient(ch, cfg)
	if err != nil {
		return err
	}

	fmt.Printf("Decoded configuration %v", cfg)

	domainName := c.extractDomainName(ch.ResolvedZone)
	addDomainRecordRequest := &alidns.AddDomainRecordRequest{
		DomainName: tea.String(domainName),
		RR:         tea.String(c.extractRecordName(ch.ResolvedFQDN, domainName)),
		Type:       tea.String("TXT"),
		Value:      tea.String(ch.Key),
	}

	_, err = dnsClient.AddDomainRecord(addDomainRecordRequest)
	if err != nil {
		return err
	}
	fmt.Printf("Decoded configuration %v", cfg)
	return nil
}

func (c *aliyunDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
	cfg, err := loadConfig(ch.Config)
	if err != nil {
		return err
	}

	// TODO: add code that sets a record in the DNS provider's console
	dnsClient, _ := c.getDnsClient(ch, cfg)

	domainName := c.extractDomainName(ch.ResolvedZone)
	deleteSubDomainRecordsRequest := &alidns.DeleteSubDomainRecordsRequest{
		DomainName: tea.String(domainName),
		Type:       tea.String("TXT"),
		RR:         tea.String(c.extractRecordName(ch.ResolvedFQDN, domainName)),
	}

	_, err = dnsClient.DeleteSubDomainRecords(deleteSubDomainRecordsRequest)
	if err != nil {
		return err
	}
	return nil
}

func (c *aliyunDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
	cl, err := kubernetes.NewForConfig(kubeClientConfig)
	if err != nil {
		return err
	}

	c.client = cl
	c.dnsClients = make(map[string]*alidns.Client)
	return nil
}

func loadConfig(cfgJSON *extapi.JSON) (aliyunDNSProviderConfig, error) {
	cfg := aliyunDNSProviderConfig{}
	if cfgJSON == nil {
		return cfg, nil
	}
	if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil {
		return cfg, fmt.Errorf("error decoding solver config: %v", err)
	}

	return cfg, nil
}

func (c *aliyunDNSProviderSolver) getDnsClient(ch *v1alpha1.ChallengeRequest, cfg aliyunDNSProviderConfig) (_result *alidns.Client, _err error) {

	accessKeyId, err := c.getRefValue(ch, cfg.AccessKey, cfg.AccessKeyRef)
	if err != nil {
		return nil, err
	}
	println(accessKeyId)

	accessKeySecret, err := c.getRefValue(ch, cfg.AccessSecret, cfg.AccessSecretRef)
	if err != nil {
		return nil, err
	}
	println(accessKeyId)

	client, ok := c.dnsClients[accessKeyId]
	if ok {
		return client, nil
	}

	config := &openapi.Config{
		AccessKeyId:     &accessKeyId,
		AccessKeySecret: &accessKeySecret,
	}

	if cfg.Endpoint == "" {
		config.Endpoint = tea.String("alidns.cn-beijing.aliyuncs.com")
	} else {
		config.Endpoint = tea.String(cfg.Endpoint)
	}

	client, err = alidns.NewClient(config)
	if err != nil {
		return nil, err
	}
	c.dnsClients[accessKeyId] = client

	// client.OpenLogger()
	return client, nil
}

func (c *aliyunDNSProviderSolver) getRefValue(ch *v1alpha1.ChallengeRequest, val string, refVal metav1.SecretKeySelector) (_result string, _err error) {
	if val == "" {
		ref := refVal
		if ref.Key == "" {
			return "", fmt.Errorf("no secret for %q in secret '%s/%s'", ref.Name, ref.Key, ch.ResourceNamespace)
		}
		if ref.Name == "" {
			return "", fmt.Errorf("no secret for %q in secret '%s/%s'", ref.Name, ref.Key, ch.ResourceNamespace)
		}

		secret, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).Get(context.TODO(), ref.Name, apis_metav1.GetOptions{})
		if err != nil {
			return "", err
		}

		result, ok := secret.Data[ref.Key]
		if !ok {
			return "", fmt.Errorf("no secret for %q in secret '%s/%s'", ref.Name, ref.Key, ch.ResourceNamespace)
		}
		return string(result), nil
	} else {
		return val, nil
	}
}

func (c *aliyunDNSProviderSolver) extractRecordName(fqdn, domain string) string {
	if idx := strings.Index(fqdn, "."+domain); idx != -1 {
		return fqdn[:idx]
	}

	return util.UnFqdn(fqdn)
}

func (c *aliyunDNSProviderSolver) extractDomainName(zone string) string {
	authZone, err := util.FindZoneByFqdn(zone, util.RecursiveNameservers)
	if err != nil {
		return zone
	}
	return util.UnFqdn(authZone)
}
